#!/usr/bin/env python
#
# graphs.py
#
# Version : 0.7
# Date : 20080529
#
# Author : Bernard Czenkusz
# Email  : bernie@skipole.co.uk

#
# graphs.py - run as part of skipolemonitor
# Copyright (c) 2008 Bernard Czenkusz
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
# for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#

"""Draws graphs for the Skipole Monitor program using PIL"""

import os
import sys
import time
import net

# Requires the Python Image library
import Image
import ImageDraw
import ImageFont


# The maximum data points to store in each item database
_MAXLINES = 2880

# For each item, two graphs are to be drawn, one to be displayed
# on the web page, and one - half the width, to be displayed in
# the gui

# The horizontal number of points
_WEBHPOINTS = 288   # giving 24 hours with 5 minute intervals
_GUIHPOINTS = 144   # and half the number for the gui graph

# Number of Pixels in a horizontal point
_PIXRATIO = 2

# Vertical number of pixels = 100
_VPIXELS = 100

# The actual image size will be greater, as there is space around the graph

_LEFT_SPACE = 50
_RIGHT_SPACE = 50
_TOP_SPACE = 20
_BOTTOM_SPACE = 50

# Days of the week
_DAYS = ("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun")


def get_time_delta():
    """Return time_now and the number of seconds between
       the start of the poll intervals and now"""
    # The polling time_interval is obtained from the net module
    time_interval= int(net.net_cfg["POLL_INTERVAL"])
    time_now = int(time.time())
    # time_delta is the time between now and the start of the last time interval
    time_delta = time_now - (time_now // time_interval)*time_interval
    return time_now, time_delta, time_interval
 

def drawgraphs(item):
    """Create two graph images, one for the web site, one for the gui."""

    global _VPIXELS
    global _LEFT_SPACE
    global _RIGHT_SPACE
    global _TOP_SPACE
    global _BOTTOM_SPACE
    global _WEBHPOINTS
    global _GUIHPOINTS
    global _PIXRATIO

    # Number of horizontal pixels for each graph
    webhpixels = _WEBHPOINTS * _PIXRATIO
    guihpixels = _GUIHPOINTS * _PIXRATIO
    # So image sizes
    web_image_size = (_LEFT_SPACE+webhpixels+_RIGHT_SPACE, _TOP_SPACE+_VPIXELS+_BOTTOM_SPACE)
    gui_image_size = (_LEFT_SPACE+guihpixels+_RIGHT_SPACE, _TOP_SPACE+_VPIXELS+_BOTTOM_SPACE)

    # Create the images - just empty rectangles
    web_im = Image.new("RGBA", web_image_size, (0,0,0,255))  # Black background
    gui_im = Image.new("RGBA", gui_image_size, (0,0,0,0))    # Black, transparent background

    # Get the current time, the time between now and the last interval
    # boundary and the interval time
    time_now,time_delta,time_interval=get_time_delta()

    # Draw the axis lines
    web_im = make_axis_lines(web_im, webhpixels, "#FFFFFF")
    gui_im = make_axis_lines(gui_im, guihpixels, "#000000")

    # Font Size
    font_size = 12
    # use a truetype font
    font_file=os.path.join(net.SCRIPTDIRECTORY,"DejaVuSansMono-Roman.ttf")
    font = ImageFont.truetype(font_file, font_size)

    # Load the 288 data points into lists time_stamp, ping_number and rtt_value
    # and also get the maximum rtt value of the 288 points

    data_file=os.path.join(net.net_cfg["NETWORK_DATA_FOLDER"],str(item),'data.cvs')
    if not os.path.exists(data_file):
        # If the database doesn't exist return silently
        return

    # Read the data file for this item and obtain:
    # web_max_rtt - the maximum rtt for the web graph - used to calculate the vertical scale
    # gui_max_rtt - the maximum rtt for the gui graph
    # time_stamp - a list of time stamps
    # ping_number - a list of ping numbers for each time stamp
    # rtt_value - a list of rtt values for each time stamp

    web_max_rtt, gui_max_rtt, time_stamp, ping_number, rtt_value = load_data(data_file,
                                                                             time_now,
                                                                             time_delta,
                                                                             time_interval)

    # Now have all data points in memory
    # Get the vertical scale
    web_max_rtt_scale, y_ax_i_w, y_ax_s_w, y_ax_p_w = vertical_scale(web_max_rtt)
    gui_max_rtt_scale, y_ax_i_g, y_ax_s_g, y_ax_p_g = vertical_scale(gui_max_rtt)

    # web_max_rtt_scale is the rtt value equivalent to the maximum y axis value - 100 pixels
    # gui_max_rtt_scale is the same for the gui graph
    # y_ax_i_w - is a tuple of the vertical grid divisions, as integer rtt values
    # y_ax_s_w - is a tuple of these divisions as strings, to be used as labels
    # y_ax_p_w - is a tuple of the divisions as pixels

    # Plot the data lines
    web_im = plot(web_im, _WEBHPOINTS, time_stamp, ping_number, rtt_value, web_max_rtt_scale, time_now, time_delta, time_interval)
    gui_im = plot(gui_im, _GUIHPOINTS, time_stamp, ping_number, rtt_value, gui_max_rtt_scale, time_now, time_delta, time_interval)

    # Release memory
    del time_stamp
    del ping_number
    del rtt_value

    # Draw the y grid lines
    web_im = y_grid(web_im, y_ax_p_w, webhpixels)
    gui_im = y_grid(gui_im, y_ax_p_g, guihpixels)

    # Draw the y axis values
    web_im = y_axis_values(web_im, y_ax_s_w, y_ax_p_w, font, "#FFFFFF")
    gui_im = y_axis_values(gui_im, y_ax_s_g, y_ax_p_g, font, "#000000")

    # Get the x axis scale, in terms of seconds between text labels,
    # between major grid lines, and minor grid lines.
    labels,major,minor=horizontal_scale(time_interval)

    # Draw the x grid lines
    web_im = x_grid(web_im, _WEBHPOINTS, major, minor, time_now, time_delta, time_interval, "#606060", "#303030")
    gui_im = x_grid(gui_im, _GUIHPOINTS, major, minor, time_now, time_delta, time_interval, "#303030", "#606060")

    # Draw the x-axis labels
    web_im = x_axis_labels(web_im, _WEBHPOINTS, labels, time_now, time_delta, time_interval, font, "#FFFFFF")
    gui_im = x_axis_labels(gui_im, _GUIHPOINTS, labels, time_now, time_delta, time_interval, font, "#000000")

    # the font is no longer required
    del font

    # Save the images
    web_im_file=os.path.join(net.net_cfg["HTTPROOT"],str(item),"log.png")
    try:
        # If a file of this name already exists, remove it
        if os.path.exists(web_im_file):
            os.remove(web_im_file)
        # and save the image
        web_im.save(web_im_file)
    except:
        # Do nothing on failure
        pass
    del web_im

    gui_im_file=os.path.join(net.net_cfg["NETWORK_DATA_FOLDER"],str(item),"gui_graph.png")
    try:
        # If a file of this name already exists, remove it
        if os.path.exists(gui_im_file):
            os.remove(gui_im_file)
        # and save the image
        gui_im.save(gui_im_file)
    except:
        # Do nothing on failure
        pass
    del gui_im

    return


def vertical_scale(max_rtt):
    """Get the maximum rtt equating to _VPIXELS of 100"""
    # return max rtt scale, a tuple of rtt axis divisions, the same as strings, the equivalent in pixels
    if max_rtt < 10 : return 10, (0,2,4,6,8,10), ("0","2","4","6","8","10"), (0,20,40,60,80,100)
    if max_rtt < 20 : return 20, (0,5,10,15,20), ("0","5","10","15","20"), (0,25,50,75,100)
    if max_rtt < 50 : return 50, (0,10,20,30,40,50), ("0","10","20","30","40","50"), (0,20,40,60,80,100)
    if max_rtt < 100 : return 100, (0,20,40,60,80,100), ("0","20","40","60","80","100"), (0,20,40,60,80,100)
    if max_rtt < 200 : return 200, (0,50,100,150,200), ("0","50","100","150","200"), (0,25,50,75,100)
    if max_rtt < 500 : return 500, (0,100,200,300,400,500), ("0","100","200","300","400","500"), (0,20,40,60,80,100)
    if max_rtt < 1000 : return 1000, (0,200,400,600,800,1000), ("0","200","400","600","800","1000"), (0,20,40,60,80,100)
    if max_rtt < 2000 : return 2000, (0,500,1000,1500,2000), ("0","500","1000","1500","2000"), (0,25,50,75,100)
    return 5000, (0,1000,2000,3000,4000,5000), ("0","1000","2000","3000","4000","5000"), (0,20,40,60,80,100)


def horizontal_scale(time_interval):
    """Get the horizontal scale, in terms of seconds between grid points
       return the seconds between labels, major grid lines, minor grid lines."""
    if time_interval == 600:
        # 6 hours, 6 hours, one hour
        return 21600, 21600, 3600
    if time_interval == 300:
        # 4 hours, 4 hours, one hour
        return 14400, 14400, 3600
    if time_interval == 120:
        # 2 hours, one hour, half hour
        return 7200, 3600, 1800
    if time_interval == 60:
        # 1 hour, 1 hour, fifteen minutes
        return 3600, 3600, 900
    # As default - treat as 5 min time_intervals
    return 14400, 14400, 3600
      

def make_axis_lines(im, hpixels, colour):
    """Draws the axis lines in the image"""
    global _LEFT_SPACE
    global _VPIXELS
    global _TOP_SPACE
    draw = ImageDraw.Draw(im)
    # Draw the axis 4 pixels longer than they need to be
    # and of width 1 pixels
    axis_extra = 4
    axis_width = 1
    # Y axis
    Y_MAX = (_LEFT_SPACE-axis_width, _TOP_SPACE-axis_extra)
    Y_MIN = (_LEFT_SPACE-axis_width, _TOP_SPACE+_VPIXELS+axis_extra+axis_width)
    draw.line([Y_MAX, Y_MIN], width=axis_width, fill=colour)
    # X axis
    X_MIN = (_LEFT_SPACE-axis_width-axis_extra, _TOP_SPACE+_VPIXELS)
    X_MAX = (_LEFT_SPACE+hpixels+axis_extra, _TOP_SPACE+_VPIXELS)
    draw.line([X_MIN, X_MAX], width=axis_width, fill=colour)
    del draw
    return im


def y_grid(im, y_ax_p, hpixels):
    """Draws the y grid lines"""
    global _LEFT_SPACE
    global _VPIXELS
    global _TOP_SPACE
    draw = ImageDraw.Draw(im)
    # Draw the lines of width 1 pixel
    grid_width = 1
    # With a bit more on the right than is actually plotted
    axis_extra = 4
    for v in y_ax_p:
        # v is zero at the axis
        if v == 0: continue
        X_MIN = (_LEFT_SPACE, _TOP_SPACE+_VPIXELS-v)
        X_MAX = (_LEFT_SPACE+hpixels+axis_extra, _TOP_SPACE+_VPIXELS-v)
        draw.line([X_MIN, X_MAX], width=grid_width, fill="#888888")
    del draw
    return im


def y_axis_values(im, y_ax_s, y_ax_p, font, colour):
    """Draws the y axis values"""
    global _LEFT_SPACE
    global _VPIXELS
    global _TOP_SPACE
    # space between text and the axis
    space = 10
    draw = ImageDraw.Draw(im)
    for i in range(len(y_ax_s)):
        # Get a tuple, width, height of the font
        text_width, text_height = font.getsize(y_ax_s[i])
        text_pos = (_LEFT_SPACE - text_width - space, _TOP_SPACE+_VPIXELS-y_ax_p[i]-(text_height//2))
        draw.text(text_pos, y_ax_s[i], font=font, fill=colour)
    del draw
    return im


def x_grid(im, hpoints, major, minor, time_now, time_delta, time_interval, maj_colour, min_colour):
    """Draws the x-axis time grid"""
    global _LEFT_SPACE
    global _VPIXELS
    global _TOP_SPACE
    # The number of horizontal pixels
    hpixels = hpoints * _PIXRATIO
    draw = ImageDraw.Draw(im)
    # Draw the lines of width 1 pixel
    grid_width = 1
    # Draw the major grid lines 4 pixels longer than they need to be
    major_extra = 4

    # Get t_max and t_min in seconds
    # t_max is the time at the right end of the graph
    t_max = time_now - time_delta
    # t_min is the time at the left end of the graph
    t_min = t_max - hpoints*time_interval

    # The major and minor grid intervals have been given in seconds
    # now need these intervals in pixels
    majorpixels = (major//time_interval)*_PIXRATIO
    minorpixels = (minor//time_interval)*_PIXRATIO

    # first grid line occurs on the 24 hour interval before t_min - this synchronises
    # grid lines with the 24 hour clock
    t_24 = (t_min // 86400)*86400
    t_tuple = time.localtime(t_24)

    if t_tuple[3] != 0:
        # local time is not on the gm time interval, so subtract the number of
        # hours required to make t_24 right on the local 24 hour boundary
        t_24 = t_24 - t_tuple[3] * 3600

    # p_major is the position of the first major grid line in pixels, note that it
    # is to the left of the y axis, and is negative - as grid intervals are added
    # they will only be drawn when they become positive, and within the graph area
    p_major = 0 - ((t_min-t_24)//time_interval)*_PIXRATIO

    # Starting from p_major, draw the grid
    while p_major <= hpixels:
        if p_major > 0:
            # Draw the major grid
            Y_MAX = (_LEFT_SPACE+p_major, _TOP_SPACE-major_extra)
            Y_MIN = (_LEFT_SPACE+p_major, _TOP_SPACE+_VPIXELS+major_extra)
            draw.line([Y_MAX, Y_MIN], width=grid_width, fill=maj_colour)
        # Get first minor grid point
        p_minor = p_major + minorpixels
        # Increment to get the next major grid point
        p_major += majorpixels
        while p_minor < p_major:
            if (p_minor > 0) and (p_minor <= hpixels):
                # Draw the minor grid
                Y_MAX = (_LEFT_SPACE+p_minor, _TOP_SPACE)
                Y_MIN = (_LEFT_SPACE+p_minor, _TOP_SPACE+_VPIXELS)
                draw.line([Y_MAX, Y_MIN], width=grid_width, fill=min_colour)
            # Increment the minor grid point
            p_minor += minorpixels

    del draw
    return im


def x_axis_labels(im, hpoints, labels, time_now, time_delta, time_interval, font, colour):
    """Draws the x-axis labels"""
    global _LEFT_SPACE
    global _VPIXELS
    global _TOP_SPACE
    global _PIXRATIO

    hpixels = hpoints * _PIXRATIO

    # Vertical space below the axis
    space = 10

    draw = ImageDraw.Draw(im)

    # Get t_max and t_min in seconds
    t_max = time_now - time_delta
    t_min = t_max - hpoints*time_interval

    # label intervals in pixels
    labelpixels = (labels//time_interval)*_PIXRATIO

    # first label occurs on the 24 hour interval before t_min
    t_label = (t_min // 86400)*86400
    t_tuple = time.localtime(t_label)

    if t_tuple[3] != 0:
        # local time is not on the gm time interval, so subtract the number of
        # hours required to make t_24 right on the local 24 hour boundary
        t_label = t_label - t_tuple[3] * 3600
    p_label = 0 - ((t_min-t_label)//time_interval)*_PIXRATIO

    # Starting from p_label, draw the labels
    while p_label <= hpixels:
        if p_label > 0:
            t_label_hours = time.localtime(t_label)
            text_time = str(t_label_hours[3]) + ":00"
            text_day = _DAYS[t_label_hours[6]]
            # Get a tuple, width, height of the text
            text_time_width, text_time_height = font.getsize(text_time)
            text_day_width, text_day_height = font.getsize(text_day)
            # Draw the label
            text_time_pos = (_LEFT_SPACE + p_label - (text_time_width//2), _TOP_SPACE+_VPIXELS+space)
            draw.text(text_time_pos, text_time, font=font, fill=colour)
            text_day_pos = (_LEFT_SPACE + p_label - (text_day_width//2), _TOP_SPACE+_VPIXELS+space+text_time_height+2)
            draw.text(text_day_pos, text_day, font=font, fill=colour)
        # Increment to get the next label point
        p_label += labelpixels
        t_label += labels

    del draw
    return im


def load_data(data_file, time_now, time_delta, time_interval):
    """Reads the data file into lists, and also gets the maximums, to be used to obtain vertical scales"""
    global _WEBHPOINTS
    global _GUIHPOINTS

    # Get t_max and t_min in seconds
    t_max = time_now - time_delta
    t_min = t_max - _WEBHPOINTS*time_interval

    fp = open(data_file,"r")
    time_stamp = []
    ping_number = []
    rtt_value = []
    web_max_rtt = 0
    gui_max_rtt = 0
    line_count = 0
    t = t_max
    for line in fp:
        a,b,c = line.split(" ")
        # a is time stamp in seconds
        # b is the number of pings
        # c is the round trip time
        a = int(a)
        while a<t:
            t = t-time_interval
        if t == a:
            time_stamp.append(a)
            ping_number.append(int(b))
            rtt = int(c)
            # Dont count yellow bars in the maximum values
            if int(b) > 2:
                if rtt>web_max_rtt: web_max_rtt = rtt
                if rtt>gui_max_rtt and line_count < _GUIHPOINTS: gui_max_rtt = rtt
            rtt_value.append(rtt)
            line_count += 1
            if line_count > _WEBHPOINTS or a <= t_min: break
    fp.close()
    # Now have all data points in memory
    return web_max_rtt, gui_max_rtt, time_stamp, ping_number, rtt_value


def plot(im, hpoints, time_stamp, ping_number, rtt_value, max_rtt_scale, time_now, time_delta, time_interval):
    """Plot the data points taken from the file"""
    global _LEFT_SPACE
    global _VPIXELS
    global _TOP_SPACE
    global _PIXRATIO

    hpixels = hpoints * _PIXRATIO
    draw = ImageDraw.Draw(im)

    # Get t_max and t_min in seconds
    t_max = time_now - time_delta
    t_min = t_max - hpoints*time_interval

    for i in range(len(time_stamp)):
        if time_stamp[i] < t_min: break
        xpixel= (time_stamp[i] - t_min)*hpixels/(t_max - t_min)
        if xpixel > 0 and xpixel <= hpixels:
            Y_MAX = (_LEFT_SPACE+xpixel, _TOP_SPACE)
            Y_MIN = (_LEFT_SPACE+xpixel, _TOP_SPACE+_VPIXELS)
            # get the y value
            if ping_number[i] == 0:
                # Draw RED line on failure
                draw.line([Y_MAX, Y_MIN], width=_PIXRATIO, fill="#FF0000")
            elif ping_number[i] < 3:
                # Dray YELLOW line on partial failure
                draw.line([Y_MAX, Y_MIN], width=_PIXRATIO, fill="#FFFF00")
            else:
                y_value = (int(rtt_value[i]) * _VPIXELS) / max_rtt_scale
                if y_value < 4: y_value = 4
                # plot the point
                Y_MAX = (_LEFT_SPACE+xpixel, _TOP_SPACE+_VPIXELS-y_value)
                draw.line([Y_MAX, Y_MIN], width=_PIXRATIO, fill="#00FF00")
    del draw
    return im


def update(host):
    """This function is called to add a ping result to the
       database for this host."""
    global _MAXLINES

    # Store the round trip time, and the number of polls
    #
    # host[0] is the host id
    # host[1] is the number of successfull pings to this host
    # host[2] is the average RTT of pings to this host
    # Update the appropriate databases

    # if data folder does not exist, create it
    if not os.path.exists(net.net_cfg["NETWORK_DATA_FOLDER"]):
        os.mkdir(net.net_cfg["NETWORK_DATA_FOLDER"])

    data_folder=os.path.join(net.net_cfg["NETWORK_DATA_FOLDER"],str(host[0]))

    if not os.path.exists(data_folder):
        os.mkdir(data_folder)
 
    data_file=os.path.join(data_folder,'data.cvs')
    temp_file=os.path.join(data_folder,'data.tmp')

    # Get the current time, the time between now and the last interval
    # boundary and the interval time
    time_now,time_delta,time_interval=get_time_delta()
    time_stamp = time_now - time_delta

    if host[1] == 0:
        line_to_add=str(time_stamp) + " 0 -1\n"
    else:
        line_to_add=str(time_stamp) + " " + str(host[1]) + " " + str(host[2]) + "\n"

    # Create file data.tmp and insert this line into it
    tempf=open(temp_file, 'w')
    tempf.write(line_to_add)

    # read lines from file data.cvs, and add them to data.tmp
    if os.path.exists(data_file):
        # read lines from file
        fp=open(data_file, 'r')
        linecount=2
        for line in fp:
            tempf.write(line)
            linecount += 1
            if linecount > _MAXLINES:
                break
        fp.close()
    tempf.close()
    # Delete original data.cvs
    if os.path.exists(data_file):
        os.remove(data_file)
    # Rename data.tmp to data.cvs
    os.rename(temp_file, data_file)
    # data_file now contains the logged data
    return



